/*
 *  This file is part of the Image Metadata Editor.
 *
 *  Copyright (C) 2015 Australian Institute of Marine Science
 *
 *  Contact: Gael Lafond <g.lafond@aims.org.au>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package au.gov.aims.exif.editor;

import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational;

import java.io.File;
import java.text.DecimalFormat;
import java.util.Comparator;

public class StringUtils {
	private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.#######");

	// NOTE: This tag is missing from imaging library. Ideally, it would be in "EAtlasField"
	//     but enums can't refer to constants defined inside their own "class".
	public static final TagInfoRational GPS_TAG_GPS_H_POSITIONING_ERROR = new TagInfoRational(
		"GPSHPositioningError", 0x001F, 1,
		TiffDirectoryType.EXIF_DIRECTORY_GPS
	);

	// Get ride of rounding errors (and trim insignificant digits)
	public static String doubleToString(final Double value) {
		return DECIMAL_FORMAT.format(value);
	}

	public static boolean isNumeric(final String s) {
		return s.matches("[-+]?\\d*\\.?\\d+");
	}

	public static String trim(final String str) {
		return str == null ? null : str.trim();
	}

	public static boolean isEmpty(final String str) {
		return str == null || str.trim().isEmpty();
	}

	public static String getFileRelativePath(final File reference, final File file) {
		// For unit tests (the CSV file may only exists in memory)
		if (reference != null) {
			return reference.toURI().relativize(file.toURI()).getPath();
		}

		return file.getAbsolutePath();
	}

	public static File getFileFromRelativePath(final File reference, final String relativePath) {
		// For unit tests (the CSV file may only exists in memory)
		if (reference == null) {
			return new File(relativePath);
		}

		File file = new File(reference, relativePath);

		// Backward compatibility - Support file which has an absolute path
		if (!file.exists()) {
			File absoluteFile = new File(relativePath);
			if (absoluteFile.exists()) {
				return absoluteFile;
			}
		}

		// Return the file, even if it do not exists. We need the file for reporting useful error messages.
		return file;
	}


	/**
	 * Case insensitive string comparator, with special case for numbers.
	 * Base algorithm slightly inspired from (no code has been copied):
	 *     http://www.java2s.com/Code/Java/Collections-Data-Structure/NaturalOrderComparator.htm
	 * NOTE: This will only returns if "str1.compareTo(str2) == 0"
	 */
	public static class NaturalOrderComparator implements Comparator<String> {
		@Override
		public int compare(String o1, String o2) {
			// Same instance or both null
			if (o1 == o2) { return 0; }
			// NULL at the end
			if (o1 == null) { return -1; }
			if (o2 == null) { return 1; }

			// Remove all white spaces, they should be ignored with this type of comparator
			String s1 = cleanupInput(o1), s2 = cleanupInput(o2);

			// Compare strings char by char
			int l1 = s1.length(), l2 = s2.length();
			char c1, c2;

			for (int i1=0, i2=0; i1<l1 && i2<l2; i1++, i2++) {
				c1 = safeCharAt(s1, i1); c2 = safeCharAt(s2, i2);

				// Process numbers
				if (Character.isDigit(c1) && Character.isDigit(c2)) {
					// Find the end character of the number (we can't compare the numbers now, we need the whole number. "9" is smaller than "12".)
					int end1 = i1, end2 = i2;
					while (end1 < l1 && Character.isDigit(safeCharAt(s1, end1))) {
						end1++;
					}
					while (end2 < l2 && Character.isDigit(safeCharAt(s2, end2))) {
						end2++;
					}

					// Get the numbers (it's a positive integer since we don't consider the "." nor "-" as been part of the number)
					int n1 = Integer.parseInt(s1.substring(i1, end1)),
						n2 = Integer.parseInt(s2.substring(i2, end2)),
						diff = n1 - n2; // Bigger number at the end

					// The numbers are different, return the difference.
					if (diff != 0) {
						return diff;
					}

					// The numbers are the same, continue.
					i1 = end1; i2 = end2;

					// The decimals
					// We want:
					//     1.01 < 02.01
					//     2.02 < 2.1
					//     2.1 = 2.10  => This won't be true for software version for example, but we are considering numbers here
					// IE: Consider the 0s immediately after the ".", ignore the one at the end of the decimal entry
					if (i1+1 < l1 && i2+1 < l2 && safeCharAt(s1, i1) == '.' && safeCharAt(s2, i2) == '.') {
						// Skip the "."
						i1++; i2++;

						// Grab the whole decimal part, without trailing 0s
						end1 = i1; end2 = i2;
						while (end1 < l1 && Character.isDigit(safeCharAt(s1, end1))) {
							end1++;
						}
						while (end2 < l2 && Character.isDigit(safeCharAt(s2, end2))) {
							end2++;
						}
						// Remove trailing 0s (.100 == .1)
						String d1 = s1.substring(i1, end1).replaceAll("0*$", ""),
							d2 = s2.substring(i2, end2).replaceAll("0*$", "");

						// Check each digits one by one (consider ".9" bigger than ".12")
						// NOTE: It's quicker and more accurate to do a char compare than a "double" comparison because
						//     floating point number always introduce small imprecision that are not trivial to
						//     consider;
						//         "double1 == double2" when "double1 - double2 < epsilon" where the value of "epsilon"
						//         depend on the precision of the 2 numbers.
						//         IE: A very small epsilon may be very large when comparing numbers close to 0.
						int digitCmp, d1l = d1.length(), d2l = d2.length();
						for (int i=0; i<d1l && i<d2l; i++) {
							digitCmp = safeCharAt(d1, i) - safeCharAt(d2, i);
							if (digitCmp != 0) {
								return digitCmp;
							}
						}

						// All digit checked are the same.
						// If one has more digit, it's therefor larger.
						int digitLengthCmp = d1l - d2l;
						if (digitLengthCmp != 0) {
							return digitLengthCmp;
						}

						i1 = end1; i2 = end2;
					}
				}

				if (i1 < l1 && i2 < l2) {
					c1 = safeCharAt(s1, i1); c2 = safeCharAt(s2, i2);

					// Process as normal characters
					// NOTE: This part is also executed after the number check
					int charCmp = Character.toLowerCase(c1) - Character.toLowerCase(c2);
					if (charCmp != 0) { return charCmp; }
				}
			}

			// The 2 strings are the same so far, check if one is longer
			int lengthCmp = l1 - l2; // We want "dog" to appear before "dogs"
			if (lengthCmp != 0) {
				return lengthCmp;
			}

			// The 2 strings are the same according to this algo, fallback to Java API
			int ignoreCaseCmp = o1.compareToIgnoreCase(o2);
			return (ignoreCaseCmp == 0) ? o1.compareTo(o2) : ignoreCaseCmp;
		}

		// Return the character at position i, or the string end character (\0) if the
		// index is off limit.
		// NOTE: This is just an extra security, it should normally never ask for a char when the index is off limit.
		private static char safeCharAt(String s, int i) {
			return i >= 0 && i < s.length() ? s.charAt(i) : 0;
		}

		/**
		 * Remove all whitespace and replace whitespaces with underscore between numbers, to avoid merging them into one large number.
		 * @param str The string to clean up
		 * @return String without whitespace, for better natural comparison.
		 */
		protected static String cleanupInput(String str) {
			return str
					.replaceAll("([0-9\\.])\\s+([0-9\\.])", "$1_$2")
					.replaceAll("\\s+", ""); // remove all whitespaces
		}
	}
}
